using static QRCoder.QRCodeGenerator;

/* This renderer is inspired by RemusVasii: https://github.com/Shane32/QRCoder/issues/223 */
namespace QRCoder;


/// <summary>
/// Represents a QR code generator that outputs QR codes as PDF byte arrays.
/// </summary>
public class PdfByteQRCode : AbstractQRCode, IDisposable
{
    private readonly byte[] _pdfBinaryComment = new byte[] { 0x25, 0xe2, 0xe3, 0xcf, 0xd3 };

    /// <summary>
    /// Initializes a new instance of the <see cref="PdfByteQRCode"/> class.
    /// Constructor without parameters to be used in COM objects connections.
    /// </summary>
    public PdfByteQRCode() { }

    /// <summary>
    /// Initializes a new instance of the <see cref="PdfByteQRCode"/> class with the specified <see cref="QRCodeData"/>.
    /// </summary>
    /// <param name="data"><see cref="QRCodeData"/> generated by the QRCodeGenerator.</param>
    public PdfByteQRCode(QRCodeData data) : base(data) { }

    /// <summary>
    /// Creates a PDF document with a black and white QR code.
    /// </summary>
    /// <param name="pixelsPerModule">The number of pixels each dark/light module of the QR code will occupy in the final QR code image.</param>
    /// <returns>Returns the QR code graphic as a PDF byte array.</returns>
    public byte[] GetGraphic(int pixelsPerModule)
        => GetGraphic(pixelsPerModule, "#000000", "#ffffff");

    /// <summary>
    /// Creates a PDF document with specified colors and DPI.
    /// </summary>
    /// <param name="pixelsPerModule">The number of pixels each dark/light module of the QR code will occupy in the final QR code image.</param>
    /// <param name="darkColorHtmlHex">The color of the dark modules in HTML hex format.</param>
    /// <param name="lightColorHtmlHex">The color of the light modules in HTML hex format.</param>
    /// <param name="dpi">The DPI (dots per inch) of the PDF document.</param>
    /// <param name="jpgQuality">The JPEG quality parameter (obsolete, no longer used).</param>
    /// <returns>Returns the QR code graphic as a PDF byte array.</returns>
    public byte[] GetGraphic(int pixelsPerModule, string darkColorHtmlHex, string lightColorHtmlHex, int dpi = 150, long jpgQuality = 85)
    {
        // Get QR code dimensions
        var moduleCount = QrCodeData.ModuleMatrix.Count;
        var imgSize = moduleCount * pixelsPerModule;
        var pdfMediaSize = ToStr(imgSize * 72 / (float)dpi);

        // Parse colors
        var darkColor = darkColorHtmlHex.HexColorToByteArray();
        var lightColor = lightColorHtmlHex.HexColorToByteArray();
        var darkColorPdf = ColorToPdfRgb(darkColor);
        var lightColorPdf = ColorToPdfRgb(lightColor);

        //Create PDF document
        using var stream = new MemoryStream();
#if NETFRAMEWORK
        var writer = new StreamWriter(stream, System.Text.Encoding.ASCII);
#elif NET6_0_OR_GREATER
        var writer = new StreamWriter(stream, System.Text.Encoding.ASCII, leaveOpen: true);
#else
        var writer = new StreamWriter(stream, System.Text.Encoding.ASCII, 1024, true);
#endif
        try
        {
            var xrefs = new List<long>();

            // PDF header - declares PDF version 1.5
            writer.Write("%PDF-1.5\r\n");
            writer.Flush();

            // Binary comment - ensures PDF is treated as binary file (prevents text mode corruption)
            stream.Write(_pdfBinaryComment, 0, _pdfBinaryComment.Length);
            writer.WriteLine();

            writer.Flush();
            xrefs.Add(stream.Position);

            // Object 1: Catalog - root of PDF document structure
            writer.Write(
                ToStr(xrefs.Count) + " 0 obj\r\n" +       // Object number and generation number (0)
                "<<\r\n" +                                // Begin dictionary
                "/Type /Catalog\r\n" +                    // Declares this as the document catalog
                "/Pages 2 0 R\r\n" +                      // References the Pages object (object 2)
                ">>\r\n" +                                // End dictionary
                "endobj\r\n"                              // End object
            );

            writer.Flush();
            xrefs.Add(stream.Position);

            // Object 2: Pages - defines page tree structure
            writer.Write(
                ToStr(xrefs.Count) + " 0 obj\r\n" +                                     // Object number and generation number (0)
                "<<\r\n" +                                                              // Begin dictionary
                "/Count 1\r\n" +                                                        // Number of pages in document
                "/Kids [ <<\r\n" +                                                      // Array of page objects - begin inline page dictionary
                    "/Type /Page\r\n" +                                                 // Declares this as a page
                    "/Parent 2 0 R\r\n" +                                               // References parent Pages object
                    "/MediaBox [0 0 " + pdfMediaSize + " " + pdfMediaSize + "]\r\n" +   // Page dimensions [x1 y1 x2 y2]
                    "/Resources << /ProcSet [ /PDF ] >>\r\n" +                          // Required resources: PDF operations only (no images)
                    "/Contents 3 0 R\r\n" +                                             // References content stream (object 3)
                    ">> ]\r\n" +                                                        // End inline page dictionary and Kids array
                ">>\r\n" +                                                              // End dictionary
                "endobj\r\n"                                                            // End object
            );

            // Content stream - PDF drawing instructions
            var scale = ToStr(imgSize * 72 / (float)dpi / moduleCount);                 // Scale factor to convert module units to PDF points
            var pathCommands = CreatePathFromModules();                                 // Create path from dark modules
            var content = "q\r\n" +                                                     // 'q' = Save graphics state
                scale + " 0 0 -" + scale + " 0 " + pdfMediaSize + " cm\r\n" +           // 'cm' = Transformation matrix: scale X, scale & flip Y, translate to top
                lightColorPdf + " rg\r\n" +                                             // 'rg' = Set RGB fill color for background
                "0 0 " + ToStr(moduleCount) + " " + ToStr(moduleCount) + " re\r\n" +    // 're' = Rectangle covering entire QR code
                "f\r\n" +                                                               // 'f' = Fill background
                darkColorPdf + " rg\r\n" +                                              // 'rg' = Set RGB fill color for dark modules
                pathCommands +                                                          // Add all dark module rectangles to path
                "f*\r\n" +                                                              // 'f*' = Fill with even-odd rule
                "Q";                                                                    // 'Q' = Restore graphics state

            writer.Flush();
            xrefs.Add(stream.Position);

            // Object 3: Content stream - contains the drawing instructions
            writer.Write(
                ToStr(xrefs.Count) + " 0 obj\r\n" +                  // Object number and generation number (0)
                "<< /Length " + ToStr(content.Length) + " >>\r\n" +  // Dictionary with stream length in bytes
                "stream\r\n" +                                       // Begin stream data
                content + "endstream\r\n" +                          // Stream content followed by end stream marker
                "endobj\r\n"                                         // End object
            );

            writer.Flush();
            var startxref = checked((int)stream.Position);

            // Cross-reference table - maps object numbers to byte offsets
            writer.Write(
                "xref\r\n" +                                   // Cross-reference table keyword
                "0 " + ToStr(xrefs.Count + 1) + "\r\n" +       // First object number (0) and count of entries
                "0000000000 65535 f\r\n"                       // Entry 0: always free, generation 65535, 'f' = free
            );

            // Write byte offset for each object
            foreach (var refValue in xrefs)
            {
                // Write each entry as a 10-digit zero-padded byte offset, 5-digit zero-padded generation number (0), and 'n' = in use
                writer.Write(checked((int)refValue).ToString("0000000000", CultureInfo.InvariantCulture) + " 00000 n\r\n");
            }

            // Trailer - provides location of catalog and xref table
            writer.Write(
                "trailer\r\n" +                                    // Trailer keyword
                "<<\r\n" +                                         // Begin trailer dictionary
                "/Size " + ToStr(xrefs.Count + 1) + "\r\n" +       // Total number of entries in xref table
                "/Root 1 0 R\r\n" +                                // Reference to catalog object
                ">>\r\n" +                                         // End trailer dictionary
                "startxref\r\n" +                                  // Start of xref keyword
                ToStr(startxref) + "\r\n" +                        // Byte offset of xref table
                "%%EOF"                                            // End of file marker
            );

            writer.Flush();
        }
        finally
        {
#if !NETFRAMEWORK
            writer.Dispose();
#endif
        }

        return stream.ToArray();
    }

    /// <summary>
    /// Creates a PDF path with rectangles for all dark modules in the QR code.
    /// Uses Run-Length Encoding (RLE) to combine adjoining dark modules in each row into single rectangles.
    /// </summary>
    /// <returns>PDF path commands as a string.</returns>
    private string CreatePathFromModules()
    {
        var pathCommands = new StringBuilder();
        var matrix = QrCodeData.ModuleMatrix;
        var size = matrix.Count;

        for (int y = 0; y < size; y++)
        {
            int x = 0;
            while (x < size)
            {
                // Skip light modules
                if (!matrix[y][x])
                {
                    x++;
                    continue;
                }

                // Found a dark module - find the run length
                int startX = x;
                while (x < size && matrix[y][x])
                {
                    x++;
                }

                // Create a single rectangle for the entire run of dark modules
                // Format: x y width height re
                pathCommands.AppendInvariant(startX);
                pathCommands.Append(' ');
                pathCommands.AppendInvariant(y);
                pathCommands.Append(' ');
                pathCommands.AppendInvariant(x - startX);
                pathCommands.Append(" 1 re\r\n");
            }
        }

        return pathCommands.ToString();
    }

    /// <summary>
    /// Converts RGB byte array to PDF color space values (0.0 to 1.0).
    /// </summary>
    /// <param name="color">RGB color as byte array.</param>
    /// <returns>PDF color string (three decimal values).</returns>
    private static string ColorToPdfRgb(byte[] color)
    {
        if (color.Length != 3)
            throw new ArgumentException("Color must be a 3-byte RGB array", nameof(color));

        const float inv255 = 1 / 255f;
        var r = ToStr(color[0] * inv255);
        var g = ToStr(color[1] * inv255);
        var b = ToStr(color[2] * inv255);

        return r + " " + g + " " + b;
    }

    /// <summary>
    /// Converts an integer to a string using invariant culture for consistent PDF formatting.
    /// </summary>
    /// <param name="value">The integer value to convert.</param>
    /// <returns>String representation of the integer.</returns>
    private static string ToStr(int value) => value.ToString(CultureInfo.InvariantCulture);

    /// <summary>
    /// Converts a float to a string using invariant culture for consistent PDF formatting.
    /// </summary>
    /// <param name="value">The float value to convert.</param>
    /// <returns>String representation of the float.</returns>
    private static string ToStr(float value) => value.ToString("G7", CultureInfo.InvariantCulture);
}

/// <summary>
/// Provides static methods for creating PDF byte array QR codes.
/// </summary>
public static class PdfByteQRCodeHelper
{
    /// <summary>
    /// Creates a PDF byte array QR code with a single function call.
    /// </summary>
    /// <param name="plainText">The text or payload to be encoded inside the QR code.</param>
    /// <param name="pixelsPerModule">The number of pixels each dark/light module of the QR code will occupy in the final QR code image.</param>
    /// <param name="darkColorHtmlHex">The color of the dark modules in HTML hex format.</param>
    /// <param name="lightColorHtmlHex">The color of the light modules in HTML hex format.</param>
    /// <param name="eccLevel">The level of error correction data.</param>
    /// <param name="forceUtf8">Specifies whether the generator should be forced to work in UTF-8 mode.</param>
    /// <param name="utf8BOM">Specifies whether the byte-order-mark should be used.</param>
    /// <param name="eciMode">Specifies which ECI mode should be used.</param>
    /// <param name="requestedVersion">Sets the fixed QR code target version.</param>
    /// <returns>Returns the QR code graphic as a PDF byte array.</returns>
    public static byte[] GetQRCode(string plainText, int pixelsPerModule, string darkColorHtmlHex,
        string lightColorHtmlHex, ECCLevel eccLevel, bool forceUtf8 = false, bool utf8BOM = false,
        EciMode eciMode = EciMode.Default, int requestedVersion = -1)
    {
        using var qrGenerator = new QRCodeGenerator();
        using var qrCodeData = qrGenerator.CreateQrCode(plainText, eccLevel, forceUtf8, utf8BOM, eciMode,
                requestedVersion);
        using var qrCode = new PdfByteQRCode(qrCodeData);
        return qrCode.GetGraphic(pixelsPerModule, darkColorHtmlHex, lightColorHtmlHex);
    }

    /// <summary>
    /// Creates a PDF byte array QR code with a single function call.
    /// </summary>
    /// <param name="txt">The text or payload to be encoded inside the QR code.</param>
    /// <param name="eccLevel">The level of error correction data.</param>
    /// <param name="size">The number of pixels each dark/light module of the QR code will occupy in the final QR code image.</param>
    /// <returns>Returns the QR code graphic as a PDF byte array.</returns>
    public static byte[] GetQRCode(string txt, ECCLevel eccLevel, int size)
    {
        using var qrGen = new QRCodeGenerator();
        using var qrCode = qrGen.CreateQrCode(txt, eccLevel);
        using var qrBmp = new PdfByteQRCode(qrCode);
        return qrBmp.GetGraphic(size);
    }
}
